from dataclasses import dataclass
import json
import requests
import os
import subprocess
import re

from collections import OrderedDict

from codesecurity.data.api import pickle_load,pickle_save
from codesecurity.data.package_extract import extract_tgz,extract_npm_code_to_dir
from codesecurity.data.couchdb_connector import CouchDbConnector
from codesecurity.utils.speed_test import compute_time_cost

# Mirrors=['https://skimdb.npmjs.com/registry',
#          'https://replicate.npmjs.com/registry']

class NpmVersionMeta:
    def __init__(self,version_data:dict):
        self.version_name = version_data.get("name")
        self.version = version_data.get("version")
        self.description = version_data.get("description")
        self.license = version_data.get("license")
        self.homepage = version_data.get("homepage")
        self.repository = version_data.get("repository")
        self.keywords = version_data.get("keywords")
        self.files = version_data.get("files")
        self.main = version_data.get("main")
        self.scripts = version_data.get("scripts")
        self.dependencies = version_data.get("dependencies")
        self.devDependencies = version_data.get("devDependencies")
        self.gitHead = version_data.get("gitHead")
        self.bugs = version_data.get("bugs")
        self._id = version_data.get("_id")
        self._npmVersion = version_data.get("_npmVersion")
        self._nodeVersion = version_data.get("_nodeVersion")
        self._npmUser = version_data.get("_npmUser")
        self.dist = version_data.get("dist")
        self.maintainers = version_data.get("maintainers")
        self._npmOperationalInternal = version_data.get("_npmOperationalInternal")

class NpmTimeMeta:
    def __init__(self,time_data:dict):
        
        except_key=['created','modified']
        
        self.created = time_data.get("created")
        self.modified = time_data.get("modified")

        self.versions_created=[]
        
        for key,value in time_data.items():
            if key not in except_key:
                self.versions_created.append((key,value))

        #self.versions_created.sort(key=lambda x:x[1])
    
    def time_after(self,time):
        for key,value in self.versions_created:
            if not isinstance(value,str):
                print(f'{value} is not str') 
                continue
            
            if value>time:
                yield key,value
                
    def time_between(self,start_time,end_time):
        for key,value in self.versions_created:
            if not isinstance(value,str):
                print(f'{value} is not str') 
                continue
            
            if start_time<=value<=end_time:
                yield key,value
        
class NpmPackageMeta:
    def __init__(self, data:dict):
        self._id = data.get("_id")
        self._rev = data.get("_rev")
        self.name = data.get("name")
        self.dist_tags = data.get("dist-tags")
        self.versions = data.get("versions")
        self.readme = data.get("readme")
        self.maintainers = data.get("maintainers")
        
        self.homepage = data.get("homepage")
        self.keywords = data.get("keywords")
        self.repository = data.get("repository")
        self.bugs = data.get("bugs")
        self.license = data.get("license")
        self.readmeFilename = data.get("readmeFilename")
        
        time = data.get("time")
        self.time_meta=NpmTimeMeta(time) if time is not None else None
        
        self.versions_meta = OrderedDict()
        if self.versions is not None:
            for version_name, version_data in self.versions.items():
                self.versions_meta[version_name] = NpmVersionMeta(version_data)
    
    @property
    def latest_version(self):
        versions=list(self.versions_meta.keys())

        return versions[-1] if len(versions)>0 else None
        
    
    @property
    def package_name(self):
        
        return self.name
    
class NpmMetaDB:
    def __init__(self,url,db_name) -> None:
        self.db_connector=CouchDbConnector(db_name,url)
    
    def list_all_name(self):
        return self.db_connector.ids()
    
    def get(self,id):
        doc=self.db_connector.get(id)

        return NpmPackageMeta(doc)
    
    def list_top_k(self,k):
        heads=self.db_connector.topk_ids(k)
        
        topkdocs=[self.get(e) for e in heads]
        
        return topkdocs
    
    def iter_docs(self,group=100,max_times=None):
        for doc in self.db_connector.iter_docs(group,max_times):
            yield NpmPackageMeta(doc)
    
    def lastest_docs(self,after,before=None):
        group=500
        start=0
        
        end_flag=False
        
        
        start_key=None
        
        result=[row for row in self.db_connector.db.view('app/updated',descending=True,include_docs=True,limit=group)]
        
        
        while len(result)==group:
            for row in result:
                start_key=row.key
                if before is not None and row.key>=before:
                    continue
                if row.key>=after:
                    yield NpmPackageMeta(row.doc)

                else:
                    end_flag=True
                    break
                
            start+=group
            
            if end_flag: break
            
            result=[row for row in self.db_connector.db.view('app/updated',descending=True,include_docs=True,limit=group,startkey=start_key)]
            # if doc.time_meta is None: continue
            # for key,value in doc.time_meta.time_after(after):
            #     yield doc,key,value

        
    @staticmethod
    def default():
        url='http://admin:GxW1996623@localhost:5984/'
        db_name='npm_skimdb'
        return NpmMetaDB(url,db_name)

class NpmHotPackageMeta:
    def __init__(self, data:dict):
        self.score = data.get("score",{})
        self.package = data.get("package",{})
        self.flags = data.get("flags",{})
        self.searchScore = data.get("searchScore",0)
        
    @property
    def package_name(self):
        return self.package['name']
    
    @property
    def author_name(self):
        return self.package['author']['name']
    
    @property
    def version(self):
        return self.package['version']
    
    @property
    def hot_score(self):
        return self.score['final']
# @dataclass
# class NpmPackageInfo:
#     key:str=""
#     id:str=""
#     rev:str=""
    
#     @staticmethod
#     def from_json(json_dict):
#         id=json_dict['id']
#         key=json_dict['key']
#         rev=json_dict['value']['rev']
        
#         return NpmPackageInfo(key=key,id=id,rev=rev)

# def default_mirror():
#     return Mirrors[0]

# def get_all_npm_packages():
#     url = f'{default_mirror()}/_all_docs'
#     response = requests.get(url, stream=True)

#     is_head=True
    
#     # 输出包名
#     for line in response.iter_lines():
#         # 过滤掉keep-alive新行
#         try:
#             if line:
#                 line=line.decode('utf-8')
#                 if is_head:
#                     is_head=False
#                     line+=']}'
#                     parsed_line = json.loads(line)
                    
#                     total_number=parsed_line['total_rows']
#                     offset=parsed_line['offset']
#                     print(f'total number: {total_number}')
#                 else:
#                     line=line[:-1]
#                     parsed_line = json.loads(line)
#                     print(f'{line}')
#                     yield NpmPackageInfo.from_json(parsed_line)
#         except:
#             print(f'illegal line: {line}')

def get_package_all_versions(package_name):
    url = f'https://registry.npmjs.org/{package_name}'

    response = requests.get(url)
    data = response.json()

    if 'versions' in data:
        versions = data['versions'].keys()
        
        return list(versions)
    
    return None
        
def get_hot_npm_packages(n):
    start=0
    size=100
    
    n=min(n,5000)
    n=max(n,size)
    
    hot_metas=[]
    
    for _ in range(n//size):
        url = f"https://api.npms.io/v2/search?q=not:deprecated&size={size}&from={start}"

        # 发送GET请求
        response = requests.get(url)

        # 检查响应状态码
        if response.status_code == 200:
            # 获取响应内容
            data = response.json()
            
            total=data['total']
            results=data['results']
            
            for package_info in results:
                hot_meta=NpmHotPackageMeta(package_info)
                print(f'{hot_meta.package_name}@{hot_meta.version} {hot_meta.hot_score}')
                hot_metas.append(hot_meta)
        start+=size
        #print(data)
        
    return hot_metas

def identify_npm_tgz(file_name):
    # according tgz file name to identify the package name and version
    
    file_name=os.path.split(file_name)[-1]
    
    file_name=file_name[:-4]
    args=file_name.split('A')
    
    if len(args)==2:
        package_name,version=args
        return package_name,version
    elif len(args)==3:
        org_name,sub_package_name,version=args
        
        package_name=f'@{org_name}/{sub_package_name}'
        
        return package_name,version 
    
    else:
        print(f'can not identify {file_name}')
        return None,None
    
    

def download_npm_package(package_name,destination_folder,version):
    command = f"npm pack {package_name}@{version} -q"
    
    if package_name.startswith('--'):
        command=f'npm pack -- {package_name}@{version} -q'
    elif package_name.startswith('-'):
        command=f'npm pack -- {package_name}@{version} -q'
    
    download_package_name = package_name.replace('/', '-')
    download_package_name = download_package_name.replace('@','')

    # 获取下载的包的文件名
    package_file_name = f"{download_package_name}-{version}.tgz"
    
    dest_package_name=package_name.replace('/','A')
    dest_package_name=dest_package_name.replace('@','')
    
    dest_package_name=f'{dest_package_name}A{version}.tgz'

    # 移动包到目标文件夹
    move_command = f'mv ./{package_file_name} {destination_folder}/{dest_package_name}'
    
    if os.path.exists(f'{destination_folder}/{dest_package_name}'):
        print(f'{package_name}@{version} already exists.')
        return f'{destination_folder}/{dest_package_name}'
    
    try:
        subprocess.run(command, shell=True, check=True)
    except:
        print(f'download {package_name} happend error.')
        return None
    
    
    subprocess.run(move_command, shell=True, check=True)

    return f'{destination_folder}/{dest_package_name}'

def download_newest_npm_package(package_name,destination_folder):
    all_versions=get_package_all_versions(package_name)
    
    newest_version=all_versions[-1] if all_versions is not None else None
    
    if newest_version is None: return None
    
    return download_npm_package(package_name,destination_folder,newest_version)

def extract_npm_package(package_file_name,destination_folder,remove_after_extract=True):
    extract_tgz(package_file_name,destination_folder)
    os.system(f'chmod -R 777 {destination_folder}')
    if remove_after_extract:
        os.remove(package_file_name)
        print(f'remove {package_file_name}')
    
def downlad_and_extract_npm_package(package_name,destination_folder,version=None,remove_after_extract=True,silent=False):
    package_file_name=None
    
    if version is None:
        versions=get_package_all_versions(package_name)
        
        if versions is None: return None
        
        version=versions[-1]
        
    package_file_name=download_npm_package(package_name,destination_folder,version)
    
    package_destination_folder=os.path.join(destination_folder,package_name,version)
        
    if package_file_name is None: return None
    
    extract_npm_package(package_file_name,package_destination_folder,remove_after_extract)
    
    return package_destination_folder



def download_hot_npm_packages(n,destination_folder):
    hot_metas=get_hot_npm_packages(n)
    
    for hot_meta in hot_metas:
        package_name=hot_meta.package_name
        version=hot_meta.version
        download_npm_package(package_name,destination_folder,version)

def download_and_extract_hot_npm_packages(n,destination_folder):
    hot_metas=get_hot_npm_packages(n)
    
    for hot_meta in hot_metas:
        package_name=hot_meta.package_name
        version=hot_meta.version
        downlad_and_extract_npm_package(package_name,destination_folder,version)
        
        
def dep_hot_npm_packages(n,destination_folder,preprocess_folder):
    # download_extract_preprocess_hot_npm_packages(n,destination_folder)
    hot_metas=get_hot_npm_packages(n)
    
    for hot_meta in hot_metas:
        package_name=hot_meta.package_name
        version=hot_meta.version
        package_dir=downlad_and_extract_npm_package(package_name,destination_folder,version)
        if package_dir:
            extract_npm_code_to_dir(package_dir,f'{preprocess_folder}/{package_name}/{version}')
        
# def store_all_npm_packages(out_file,max_number=None):
    
#     result={}
    
#     # if os.path.exists(out_file):
#     #     result=pickle_load(out_file)
    
#     packages_iterator=get_all_npm_packages()
    
#     count=0
    
#     for package_info in packages_iterator:
#         if package_info.key in result: continue
#         result[package_info.key]=package_info
#         count+=1
        
#         if max_number!=None and count>=max_number: break
    
#     pickle_save(result,out_file)

#get_all_npm_packages()
#get_package_all_versions('jquery')

# def load_npm_packages(path):
#     return pickle_load(path)
